package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net"
	"os"
	"strings"
	"time"

	"github.com/avast/retry-go"
	log "github.com/sirupsen/logrus"
)

type NetworkSettings struct {
	Listen string
}

type Config struct {
	NetworkSettings NetworkSettings        `json:"network,omitempty" mapstructure:"network"`
	StaticEntries   map[string]interface{} `json:"static_entries,omitempty" mapstructure:"staticEntries"`
	TimestampScale  int64                  `json:"timestamp_scale,omitempty" mapstructure:"timestampScale"`
}

var config = Config{}

func main() {
	err := Viper.Unmarshal(&config)
	if err != nil {
		log.WithError(err).Fatal("failed load config")
	}
	if config.TimestampScale == 0 {
		config.TimestampScale = 1
	}

	c := cache{}

	for k, v := range config.StaticEntries {
		err := c.setEntry(k, entry{
			Value:  v,
			Static: true,
		})
		if err != nil {
			log.WithError(err).Fatal("failed set static entries")
			os.Exit(-1)
		}
	}

	go sendLoop(&c)

	listen := strings.TrimSpace(config.NetworkSettings.Listen)
	if listen == "" {
		log.Fatal("listen address is empty")
		os.Exit(-2)
	}
	var network string
	var address string
	parts := strings.Split(listen, "://")
	if len(parts) == 0 || len(parts) > 2 {
		log.WithFields(log.Fields{"listen": listen}).Fatal("invalid listen config")
		os.Exit(-3)
	} else if len(parts) == 1 {
		network = "tcp"
		address = parts[0]
	} else {
		network = parts[0]
		address = parts[1]
	}
	lis, err := net.Listen(network, address)
	if err != nil {
		log.WithFields(log.Fields{"err": err.Error(), "config": config.NetworkSettings}).Fatal("failed listen")
		os.Exit(-3)
	}
	for {
		conn, err := lis.Accept()
		if err != nil {
			log.WithFields(log.Fields{"err": err.Error()}).Error("connection errr")
		}
		go handleConnection(conn, &c)
	}
}

type Message struct {
	Name      string
	Tags      map[string]string
	Fields    map[string]interface{}
	Timestamp int64
}

func handleConnection(conn net.Conn, c *cache) {
	log.WithFields(log.Fields{
		"localAddr": conn.LocalAddr().Network,
	}).Info("handle connection")
	for {
		msg := Message{}
		decoder := json.NewDecoder(conn)
		err := decoder.Decode(&msg)
		if err == io.EOF {
			return
		} else if err != nil {
			log.WithFields(log.Fields{"err": err.Error()}).Info("failed read msg")
		} else {
			err := handlePayload(c, msg)
			if err != nil {
				log.WithFields(log.Fields{"err": err.Error(), "msg": msg}).Error("failed handle message")
			} else {
				log.WithFields(log.Fields{"msg": msg}).Info("msg processed")
			}
		}
	}
}

func handlePayload(c *cache, msg Message) error {
	timestamp := msg.Timestamp * config.TimestampScale
	t := time.Unix(timestamp/int64(time.Nanosecond), timestamp%int64(time.Nanosecond))
	for k, v := range msg.Tags {
		err := c.setEntry(k, entry{
			Value:     v,
			Timestamp: t,
		})
		if err != nil {
			log.WithFields(log.Fields{"err": err.Error(), "key": k, "value": v}).Error("failed set entry")
		}
	}
	for k, v := range msg.Fields {
		err := c.setEntry(k, entry{
			Value:     v,
			Timestamp: t,
		})
		if err != nil {
			log.WithFields(log.Fields{"err": err.Error(), "key": k, "value": v}).Error("failed set entry")
		}
	}
	return nil
}

func sendLoop(c *cache) {
	duration := time.Second * 60
	ticker := time.NewTicker(time.Second * 60)
	for t := range ticker.C {
		log.WithFields(log.Fields{"tick": t}).Info("do ticker")
		deadline := time.Now().Add(duration * -1)
		send(c, deadline, time.Now())
	}
}

func send(c *cache, deadline time.Time, now time.Time) {
	msg, err := buildMessage(c, deadline, now)
	if err != nil {
		log.WithFields(log.Fields{"err": err.Error()}).Error("failed build msg")
		return
	}
	address := Viper.GetString("dest.addr")
	if address == "" {
		log.WithFields(log.Fields{"err": "no destination configured"}).Error("failed send payload")
		return
	}
	conn, err := net.Dial("udp", address)
	if err != nil {
		return
	}
	defer conn.Close()
	err = retry.Do(func() error {
		n, err := conn.Write(msg)
		if err != nil || n != len(msg) {
			log.WithFields(log.Fields{"err": err.Error(), "written": n}).Error("failed send payload")
			return err
		}
		log.WithFields(log.Fields{"msg": string(msg)}).Info("write payload")
		return nil
	}, retry.Attempts(3), retry.Delay(time.Second))
	if err != nil {
		log.WithFields(log.Fields{"err": err.Error()}).Error("failed send message")
	}
}

func buildMessage(c *cache, deadline time.Time, now time.Time) ([]byte, error) {
	m := map[string]interface{}{}
	for k, v := range c.getAll() {
		if v.Static || !v.Timestamp.Before(deadline) {
			m[k] = v.Value
		}
	}
	m["time"] = now
	var ida interface{}
	var ok bool
	if ida, ok = m["id"]; !ok {
		return nil, fmt.Errorf("no id configured")
	}
	id := fmt.Sprintf("%v", ida)
	ma := map[string]interface{}{
		"id":   id,
		"data": m,
	}
	bs, err := json.Marshal(ma)
	return bs, err
}
